"""BLE Broadcast Manager for optimized device discovery.

Handles BLE advertisement configuration, custom broadcast data,
power management, and multi-device environment optimization.
"""

import asyncio
import logging
import random
import json
import time
from enum import Enum
from typing import Dict, List, Optional, Any, Callable
from dataclasses import dataclass, field
from pathlib import Path
import dbus
import dbus.service
import dbus.mainloop.glib

logger = logging.getLogger(__name__)


class PowerMode(Enum):
    """Power management modes for BLE broadcasting."""
    NORMAL = "NORMAL"          # Standard mode: fast discovery
    LOW_POWER = "LOW_POWER"    # Low power: extended battery life
    ADAPTIVE = "ADAPTIVE"      # Adaptive: adjust based on activity


class DeviceType(Enum):
    """Device type identifiers."""
    PRINTER = 0x01
    SCANNER = 0x02
    MFP = 0x03  # Multi-function printer
    UNKNOWN = 0xFF


class DeviceStatus(Enum):
    """Device operational status."""
    IDLE = 0x00
    BUSY = 0x01
    ERROR = 0x02
    OFFLINE = 0x03
    MAINTENANCE = 0x04


@dataclass
class BroadcastConfig:
    """Configuration for BLE broadcasting."""
    # Advertising intervals (milliseconds)
    min_interval: int = 100
    max_interval: int = 500
    low_power_interval: int = 1000
    
    # Transmit power (dBm)
    tx_power_levels: List[int] = field(default_factory=lambda: [-21, -15, -7, 1, 9])
    default_tx_power: int = -7
    low_power_tx_power: int = -15
    
    # General settings
    timeout: int = 0  # 0 = continuous broadcast
    connectable: bool = True
    discoverable: bool = True
    use_random_address: bool = False
    
    # Power mode
    power_mode: PowerMode = PowerMode.NORMAL
    
    # Adaptive mode settings
    idle_threshold: int = 60  # Seconds before switching to low power
    connection_threshold: int = 5  # Max connections before reducing broadcast
    scan_request_window: int = 10  # Scan requests to maintain high frequency
    battery_threshold: int = 20  # Battery % to force low power


@dataclass
class DeviceInfo:
    """Device information for broadcasting."""
    device_id: str
    device_name: str = "CloudPrinter"
    model: str = "CP-100"
    device_type: DeviceType = DeviceType.PRINTER
    firmware_version: str = "1.0.0"
    hardware_version: str = "1.0"
    manufacturer: str = "CloudPrint Inc."
    serial_number: str = ""
    capabilities: int = 0x07  # Bitmap of device capabilities


@dataclass
class BroadcastStats:
    """Statistics for broadcast performance monitoring."""
    start_time: float = field(default_factory=time.time)
    advertisements_sent: int = 0
    scan_requests_received: int = 0
    connections_established: int = 0
    last_activity_time: float = field(default_factory=time.time)
    power_mode_changes: int = 0
    errors: int = 0


class Advertisement(dbus.service.Object):
    """DBus advertisement object for BLE broadcasting."""
    
    PATH_BASE = '/org/bluez/cloudprinter/advertisement'
    
    def __init__(self, bus, index, device_info: DeviceInfo, config: BroadcastConfig):
        """Initialize advertisement object.
        
        Args:
            bus: DBus connection
            index: Advertisement index
            device_info: Device information to broadcast
            config: Broadcast configuration
        """
        self.path = f"{self.PATH_BASE}{index}"
        self.bus = bus
        self.device_info = device_info
        self.config = config
        
        # Advertisement data
        self.ad_type = 'peripheral'
        self.service_uuids = ['FF10', 'FF20', 'FF30']  # Service UUIDs
        self.local_name = self._generate_device_name()
        self.manufacturer_data = self._generate_manufacturer_data()
        self.service_data = {}
        self.include_tx_power = True
        
        super().__init__(bus, self.path)
    
    def _generate_device_name(self) -> str:
        """Generate device name for broadcasting."""
        # Use last 4 characters of device ID for uniqueness
        suffix = self.device_info.device_id[-4:] if self.device_info.device_id else "0000"
        return f"{self.device_info.device_name}-{suffix}"
    
    def _generate_manufacturer_data(self) -> Dict[int, List[int]]:
        """Generate manufacturer specific data."""
        # Company ID 0xFFFF for testing
        company_id = 0xFFFF
        
        # Pack device information into manufacturer data
        data = [
            self.device_info.device_type.value,
            DeviceStatus.IDLE.value,  # Current status
            (int(self.device_info.firmware_version.split('.')[0]) << 4) | 
            int(self.device_info.firmware_version.split('.')[1]),
            self.device_info.capabilities
        ]
        
        return {company_id: data}
    
    @dbus.service.method('org.bluez.LEAdvertisement1',
                        in_signature='',
                        out_signature='')
    def Release(self):
        """Release advertisement."""
        logger.info(f"Advertisement released: {self.path}")
    
    @dbus.service.method('org.bluez.LEAdvertisement1',
                        in_signature='',
                        out_signature='a{sv}')
    def GetAll(self, interface):
        """Get all advertisement properties."""
        properties = {
            'Type': dbus.String(self.ad_type),
            'ServiceUUIDs': dbus.Array(self.service_uuids, signature='s'),
            'LocalName': dbus.String(self.local_name),
            'ManufacturerData': dbus.Dictionary(self.manufacturer_data, signature='qay'),
            'IncludeTxPower': dbus.Boolean(self.include_tx_power)
        }
        
        if self.service_data:
            properties['ServiceData'] = dbus.Dictionary(self.service_data, signature='say')
        
        return properties


class BroadcastManager:
    """Manager for BLE broadcasting and device discovery."""
    
    ADAPTER_INTERFACE = 'org.bluez.Adapter1'
    LE_ADVERTISING_MANAGER_INTERFACE = 'org.bluez.LEAdvertisingManager1'
    
    def __init__(self, device_info: Optional[DeviceInfo] = None,
                 config: Optional[BroadcastConfig] = None):
        """Initialize broadcast manager.
        
        Args:
            device_info: Device information for broadcasting
            config: Broadcast configuration
        """
        self.device_info = device_info or DeviceInfo(
            device_id=self._generate_device_id(),
            device_name="CloudPrinter"
        )
        self.config = config or BroadcastConfig()
        
        # DBus setup
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
        self.bus = dbus.SystemBus()
        
        # Manager state
        self.adapter = None
        self.ad_manager = None
        self.advertisement = None
        self.is_advertising = False
        
        # Statistics
        self.stats = BroadcastStats()
        
        # Callbacks
        self.scan_callback: Optional[Callable] = None
        self.connection_callback: Optional[Callable] = None
        
        # Power management
        self.idle_timer: Optional[asyncio.Task] = None
        self.current_connections = 0
        
        logger.info(f"Broadcast manager initialized for device: {self.device_info.device_id}")
    
    def _generate_device_id(self) -> str:
        """Generate unique device ID."""
        import uuid
        return str(uuid.uuid4()).replace('-', '')[:16].upper()
    
    async def initialize(self) -> bool:
        """Initialize BLE adapter and advertising manager.
        
        Returns:
            True if initialization successful
        """
        try:
            # Get adapter
            self.adapter = self._get_adapter()
            if not self.adapter:
                logger.error("No BLE adapter found")
                return False
            
            # Get advertising manager
            self.ad_manager = dbus.Interface(
                self.bus.get_object('org.bluez', '/org/bluez/hci0'),
                self.LE_ADVERTISING_MANAGER_INTERFACE
            )
            
            # Power on adapter
            adapter_props = dbus.Interface(self.adapter, 'org.freedesktop.DBus.Properties')
            adapter_props.Set(self.ADAPTER_INTERFACE, 'Powered', dbus.Boolean(True))
            
            # Set adapter properties
            adapter_props.Set(self.ADAPTER_INTERFACE, 'Discoverable', dbus.Boolean(True))
            adapter_props.Set(self.ADAPTER_INTERFACE, 'Pairable', dbus.Boolean(True))
            
            logger.info("BLE adapter initialized successfully")
            return True
            
        except Exception as e:
            logger.error(f"Failed to initialize BLE adapter: {e}")
            return False
    
    def _get_adapter(self):
        """Get BLE adapter object."""
        try:
            remote_om = dbus.Interface(
                self.bus.get_object('org.bluez', '/'),
                'org.freedesktop.DBus.ObjectManager'
            )
            objects = remote_om.GetManagedObjects()
            
            for o, props in objects.items():
                if self.ADAPTER_INTERFACE in props:
                    return self.bus.get_object('org.bluez', o)
            
            return None
            
        except Exception as e:
            logger.error(f"Error getting adapter: {e}")
            return None
    
    async def start_advertising(self) -> bool:
        """Start BLE advertising.
        
        Returns:
            True if advertising started successfully
        """
        try:
            if self.is_advertising:
                logger.warning("Already advertising")
                return True
            
            # Create advertisement
            self.advertisement = Advertisement(
                self.bus, 0, self.device_info, self.config
            )
            
            # Register advertisement
            self.ad_manager.RegisterAdvertisement(
                self.advertisement.path,
                {}
            )
            
            self.is_advertising = True
            self.stats.advertisements_sent += 1
            
            # Start idle timer if in adaptive mode
            if self.config.power_mode == PowerMode.ADAPTIVE:
                await self._start_idle_timer()
            
            logger.info(f"Started advertising as: {self.advertisement.local_name}")
            return True
            
        except Exception as e:
            logger.error(f"Failed to start advertising: {e}")
            self.stats.errors += 1
            return False
    
    async def stop_advertising(self) -> bool:
        """Stop BLE advertising.
        
        Returns:
            True if advertising stopped successfully
        """
        try:
            if not self.is_advertising:
                logger.warning("Not currently advertising")
                return True
            
            # Unregister advertisement
            if self.ad_manager and self.advertisement:
                self.ad_manager.UnregisterAdvertisement(self.advertisement.path)
            
            self.is_advertising = False
            
            # Cancel idle timer
            if self.idle_timer:
                self.idle_timer.cancel()
                self.idle_timer = None
            
            logger.info("Stopped advertising")
            return True
            
        except Exception as e:
            logger.error(f"Failed to stop advertising: {e}")
            self.stats.errors += 1
            return False
    
    async def set_power_mode(self, mode: PowerMode) -> bool:
        """Set power management mode.
        
        Args:
            mode: Power mode to set
            
        Returns:
            True if mode set successfully
        """
        try:
            old_mode = self.config.power_mode
            self.config.power_mode = mode
            
            # Apply mode-specific settings
            if mode == PowerMode.LOW_POWER:
                await self._apply_low_power_settings()
            elif mode == PowerMode.NORMAL:
                await self._apply_normal_settings()
            elif mode == PowerMode.ADAPTIVE:
                await self._start_idle_timer()
            
            self.stats.power_mode_changes += 1
            logger.info(f"Power mode changed from {old_mode} to {mode}")
            return True
            
        except Exception as e:
            logger.error(f"Failed to set power mode: {e}")
            return False
    
    async def _apply_low_power_settings(self):
        """Apply low power mode settings."""
        # Set longer advertising interval
        await self._set_advertising_interval(
            self.config.low_power_interval,
            self.config.low_power_interval * 2
        )
        
        # Reduce transmit power
        await self._set_tx_power(self.config.low_power_tx_power)
        
        logger.debug("Applied low power settings")
    
    async def _apply_normal_settings(self):
        """Apply normal mode settings."""
        # Set normal advertising interval
        await self._set_advertising_interval(
            self.config.min_interval,
            self.config.max_interval
        )
        
        # Set normal transmit power
        await self._set_tx_power(self.config.default_tx_power)
        
        logger.debug("Applied normal settings")
    
    async def _set_advertising_interval(self, min_interval: int, max_interval: int):
        """Set advertising interval.
        
        Args:
            min_interval: Minimum interval in milliseconds
            max_interval: Maximum interval in milliseconds
        """
        try:
            # Convert to BLE units (0.625ms)
            min_units = int(min_interval / 0.625)
            max_units = int(max_interval / 0.625)
            
            # Ensure valid range (20ms to 10.24s)
            min_units = max(32, min(16384, min_units))
            max_units = max(min_units, min(16384, max_units))
            
            # This would require kernel-level access or vendor-specific commands
            # For now, log the intention
            logger.debug(f"Set advertising interval: {min_interval}-{max_interval}ms")
            
        except Exception as e:
            logger.error(f"Failed to set advertising interval: {e}")
    
    async def _set_tx_power(self, power: int):
        """Set transmit power.
        
        Args:
            power: Transmit power in dBm
        """
        try:
            # Ensure power is in valid range
            if power not in self.config.tx_power_levels:
                # Find nearest valid power level
                power = min(self.config.tx_power_levels,
                          key=lambda x: abs(x - power))
            
            # This would require kernel-level access or vendor-specific commands
            # For now, log the intention
            logger.debug(f"Set transmit power: {power} dBm")
            
        except Exception as e:
            logger.error(f"Failed to set transmit power: {e}")
    
    async def _start_idle_timer(self):
        """Start idle timer for adaptive power mode."""
        if self.idle_timer:
            self.idle_timer.cancel()
        
        self.idle_timer = asyncio.create_task(self._idle_timer_task())
    
    async def _idle_timer_task(self):
        """Idle timer task for adaptive power mode."""
        try:
            await asyncio.sleep(self.config.idle_threshold)
            
            # Check if still idle
            time_since_activity = time.time() - self.stats.last_activity_time
            if time_since_activity >= self.config.idle_threshold:
                # Switch to low power mode
                await self._apply_low_power_settings()
                logger.info("Switched to low power mode due to inactivity")
                
        except asyncio.CancelledError:
            pass
        except Exception as e:
            logger.error(f"Idle timer error: {e}")
    
    def on_scan_request(self):
        """Handle scan request from remote device."""
        self.stats.scan_requests_received += 1
        self.stats.last_activity_time = time.time()
        
        # Reset idle timer in adaptive mode
        if self.config.power_mode == PowerMode.ADAPTIVE:
            asyncio.create_task(self._start_idle_timer())
        
        # Switch to normal mode if in low power
        if self.config.power_mode == PowerMode.LOW_POWER:
            asyncio.create_task(self._apply_normal_settings())
        
        # Call user callback if set
        if self.scan_callback:
            self.scan_callback()
        
        logger.debug("Scan request received")
    
    def on_connection(self, connected: bool):
        """Handle connection state change.
        
        Args:
            connected: True if connected, False if disconnected
        """
        if connected:
            self.current_connections += 1
            self.stats.connections_established += 1
        else:
            self.current_connections = max(0, self.current_connections - 1)
        
        self.stats.last_activity_time = time.time()
        
        # Check connection threshold for adaptive mode
        if (self.config.power_mode == PowerMode.ADAPTIVE and 
            self.current_connections >= self.config.connection_threshold):
            # Reduce advertising when many connections
            asyncio.create_task(self._apply_low_power_settings())
        
        # Call user callback if set
        if self.connection_callback:
            self.connection_callback(connected)
        
        logger.info(f"Connection {'established' if connected else 'terminated'}. "
                   f"Current connections: {self.current_connections}")
    
    def set_device_name(self, name: str):
        """Set custom device name.
        
        Args:
            name: Device name to broadcast
        """
        self.device_info.device_name = name
        
        # Update advertisement if active
        if self.advertisement:
            self.advertisement.local_name = self.advertisement._generate_device_name()
            logger.info(f"Device name updated to: {name}")
    
    def set_device_info(self, **kwargs):
        """Update device information.
        
        Args:
            **kwargs: Device info fields to update
        """
        for key, value in kwargs.items():
            if hasattr(self.device_info, key):
                setattr(self.device_info, key, value)
        
        # Update advertisement if active
        if self.advertisement:
            self.advertisement.manufacturer_data = self.advertisement._generate_manufacturer_data()
            logger.info(f"Device info updated: {kwargs}")
    
    def get_stats(self) -> Dict[str, Any]:
        """Get broadcast statistics.
        
        Returns:
            Dictionary of statistics
        """
        uptime = time.time() - self.stats.start_time
        
        return {
            "uptime_seconds": int(uptime),
            "advertisements_sent": self.stats.advertisements_sent,
            "scan_requests_received": self.stats.scan_requests_received,
            "connections_established": self.stats.connections_established,
            "current_connections": self.current_connections,
            "power_mode": self.config.power_mode.value,
            "power_mode_changes": self.stats.power_mode_changes,
            "errors": self.stats.errors,
            "last_activity": datetime.fromtimestamp(self.stats.last_activity_time).isoformat()
        }
    
    async def cleanup(self):
        """Clean up resources."""
        try:
            # Stop advertising
            if self.is_advertising:
                await self.stop_advertising()
            
            # Cancel timers
            if self.idle_timer:
                self.idle_timer.cancel()
            
            logger.info("Broadcast manager cleaned up")
            
        except Exception as e:
            logger.error(f"Cleanup error: {e}")


# Multi-device environment handler
class MultiDeviceHandler:
    """Handler for multi-device BLE environments."""
    
    def __init__(self, broadcast_manager: BroadcastManager):
        """Initialize multi-device handler.
        
        Args:
            broadcast_manager: Broadcast manager instance
        """
        self.broadcast_manager = broadcast_manager
        self.discovered_devices: Dict[str, Dict] = {}
        self.collision_count = 0
        self.backoff_delay = 0
    
    async def handle_collision(self):
        """Handle advertising collision."""
        self.collision_count += 1
        
        # Calculate backoff delay
        self.backoff_delay = min(
            1000,  # Max 1 second
            random.randint(0, 50) * (1.5 ** self.collision_count)
        )
        
        logger.warning(f"Collision detected. Backing off for {self.backoff_delay}ms")
        
        # Stop and restart advertising with delay
        await self.broadcast_manager.stop_advertising()
        await asyncio.sleep(self.backoff_delay / 1000)
        await self.broadcast_manager.start_advertising()
    
    def filter_devices(self, devices: List[Dict]) -> List[Dict]:
        """Filter discovered devices.
        
        Args:
            devices: List of discovered devices
            
        Returns:
            Filtered device list
        """
        filtered = []
        current_time = time.time()
        
        for device in devices:
            # Check RSSI threshold
            if device.get('rssi', -100) < -80:
                continue
            
            # Check for duplicates
            device_id = device.get('address')
            if device_id in self.discovered_devices:
                last_seen = self.discovered_devices[device_id].get('last_seen', 0)
                if current_time - last_seen < 5:  # 5 second duplicate timeout
                    continue
            
            # Check service filter
            services = device.get('services', [])
            if not any(s in ['FF10', 'FF20', 'FF30'] for s in services):
                continue
            
            # Update discovered devices
            self.discovered_devices[device_id] = {
                **device,
                'last_seen': current_time
            }
            filtered.append(device)
        
        # Limit list size
        if len(filtered) > 50:
            filtered = filtered[:50]
        
        return filtered
    
    def cleanup_old_devices(self):
        """Remove old devices from discovered list."""
        current_time = time.time()
        timeout = 60  # Remove devices not seen for 60 seconds
        
        self.discovered_devices = {
            device_id: info
            for device_id, info in self.discovered_devices.items()
            if current_time - info.get('last_seen', 0) < timeout
        }


# Example usage
if __name__ == "__main__":
    import asyncio
    from datetime import datetime
    
    async def main():
        """Test broadcast manager."""
        # Create device info
        device_info = DeviceInfo(
            device_id="1234567890ABCDEF",
            device_name="TestPrinter",
            model="CP-100",
            firmware_version="2.0.0"
        )
        
        # Create broadcast config
        config = BroadcastConfig(
            power_mode=PowerMode.ADAPTIVE,
            min_interval=100,
            max_interval=500
        )
        
        # Create broadcast manager
        manager = BroadcastManager(device_info, config)
        
        # Initialize
        if await manager.initialize():
            print("✓ BLE adapter initialized")
            
            # Start advertising
            if await manager.start_advertising():
                print(f"✓ Started advertising as: {device_info.device_name}")
                
                # Simulate scan requests
                for i in range(3):
                    await asyncio.sleep(2)
                    manager.on_scan_request()
                    print(f"  Scan request {i+1} handled")
                
                # Simulate connection
                manager.on_connection(True)
                print("✓ Connection established")
                
                # Get stats
                stats = manager.get_stats()
                print(f"\nStatistics:")
                for key, value in stats.items():
                    print(f"  {key}: {value}")
                
                # Wait a bit
                await asyncio.sleep(5)
                
                # Stop advertising
                await manager.stop_advertising()
                print("✓ Stopped advertising")
            
            # Cleanup
            await manager.cleanup()
    
    # Run test
    asyncio.run(main())